Android Core
ComponentsThe four pillars every app is built on
Activity, Service, BroadcastReceiver, and ContentProvider. These are the only entry points into an Android application. Everything else — ViewModels, Fragments, Compose — builds on top of them.
Unlike desktop or web apps that start from a single entry point and run linearly, Android is built around a component model. The OS can instantiate any component independently — it can start your app's Service from a notification without ever showing an Activity. It can receive data through a ContentProvider without any UI at all. This design enables deep system integration, inter-app communication, and efficient resource management.
Every component must be declared in the AndroidManifest before the system can use it. The manifest is the contract between your app and the OS. Undeclared components simply don't exist as far as the system is concerned.
SELECT A COMPONENT ABOVE TO SEE ITS ROLE
The key mental model: Think of components as separately addressable objects the OS holds references to. The OS can start any component independently, kill it when resources are needed, and restart it later. Your app does not "run" — specific components are activated when needed and deactivated when done.
The Activity lifecycle — 7 callbacks
Every Activity instance moves through a defined sequence of states. The OS calls these methods to signal transitions. Implementing them correctly is the difference between an app that feels solid and one that leaks memory, loses data, or crashes on rotation.
class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() private var _binding: ActivityMainBinding? = null private val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // ViewModel survives rotation — data already loaded viewModel.uiState.observe(this) { state -> render(state) } // Restore scroll position, selected tab, etc. savedInstanceState?.let { binding.recycler.scrollToPosition(it.getInt("scroll_pos", 0)) } } override fun onResume() { super.onResume() // Acquire exclusive resources (camera, mic, sensors) } override fun onPause() { super.onPause() // FAST — release exclusive resources, save lightweight critical state } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt("scroll_pos", binding.recycler.computeVerticalScrollOffset()) } override fun onDestroy() { _binding = null // prevent memory leak super.onDestroy() } }
onDestroy is not guaranteed. If the system kills your process to reclaim memory, onDestroy does not fire. Never rely on it for critical data saves. Use onPause or onSaveInstanceState for UI state, and Room/DataStore for persistent data.
Task & back stack — how Activities are managed
Activities are organized into Tasks — stacks of Activities that represent a user's workflow. When you start a new Activity, it's pushed onto the stack. Pressing Back pops it. The app icon in Recents represents one Task. You can control this behavior with launch modes in the manifest.
Three types of Services
startService(). Runs until it calls stopSelf() or another component calls stopService(). Use for one-off background tasks. The modern alternative is WorkManager.bindService(). Offers an interface (IBinder) that other components call. Lives as long as it has at least one client bound. Useful for communicating between app components.class MusicPlayerService : Service() { private val binder = LocalBinder() private var mediaPlayer: MediaPlayer? = null inner class LocalBinder : Binder() { fun getService() = this@MusicPlayerService } override fun onCreate() { super.onCreate() mediaPlayer = MediaPlayer() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = buildMediaNotification() startForeground(NOTIFICATION_ID, notification) // promote to foreground mediaPlayer?.start() return START_STICKY // restart if killed, re-deliver last intent } override fun onBind(intent: Intent): IBinder = binder override fun onDestroy() { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null super.onDestroy() } }
Service vs WorkManager: For most background work that doesn't need user-visibility, prefer WorkManager. It handles process death, battery optimization (Doze mode), network constraints, and retries automatically. Use a Service directly only for long-running user-initiated work that needs a foreground notification, or for IPC between components.
START_STICKY, START_NOT_STICKY, START_REDELIVER_INTENT
The return value of onStartCommand() tells Android what to do if the process is killed while the Service is running. This is the restart policy:
Manifest vs Dynamic registration
registerReceiver(), unregistered with unregisterReceiver(). Only active while the component is alive. Symmetric: register in onStart, unregister in onStop. Works for all broadcast types including implicit.// ── Manifest-registered (AndroidManifest.xml) ───────────────── // <receiver android:name=".BootReceiver" android:exported="false"> // <intent-filter> // <action android:name="android.intent.action.BOOT_COMPLETED"/> // </intent-filter> // </receiver> class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { // Schedule WorkManager jobs that run on every boot WorkManager.getInstance(context).enqueue(OneTimeWorkRequest...) // NEVER do long work here — 10 second limit } } } // ── Dynamic registration (in Activity) ──────────────────────── class MainActivity : AppCompatActivity() { private val networkReceiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { viewModel.onNetworkChanged() } } override fun onStart() { super.onStart() val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) registerReceiver(networkReceiver, filter) } override fun onStop() { unregisterReceiver(networkReceiver) // always symmetric super.onStop() } } // ── goAsync() — for work that needs slightly more time ───────── class DataReceiver : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { val pendingResult = goAsync() // extends to ~30s, still not for I/O CoroutineScope(Dispatchers.Default).launch { processData(intent) pendingResult.finish() // must call finish or system kills it } } }
Android 8+ background restrictions: Most implicit broadcasts can no longer be received via the manifest. Your receiver simply won't fire. The solution is dynamic registration (while the app is open) or WorkManager constraints (to respond to network, battery, etc. changes while app is in background). A small list of exceptions (like BOOT_COMPLETED) still work via manifest.
content://authority/path/id — e.g., content://contacts/people/1. Authority uniquely identifies your provider across the system.class BookProvider : ContentProvider() { private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI("com.example.books", "books", 1) // all books addURI("com.example.books", "books/#", 2) // specific book by ID } override fun onCreate(): Boolean { // Initialize database. Called on main thread — be fast. return true } override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? { return when (uriMatcher.match(uri)) { 1 -> db.query("books", projection, selection, selectionArgs, null, null, sortOrder) 2 -> db.query("books", projection, "_id=?", arrayOf(uri.lastPathSegment!!), null, null, null) else -> throw IllegalArgumentException("Unknown URI: $uri") } } override fun insert(uri: Uri, values: ContentValues?): Uri? { ... } override fun update(uri: Uri, values: ContentValues?, sel: String?, selArgs: Array<String>?): Int = 0 override fun delete(uri: Uri, sel: String?, selArgs: Array<String>?): Int = 0 override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) { 1 -> "vnd.android.cursor.dir/vnd.example.books" 2 -> "vnd.android.cursor.item/vnd.example.books" else -> null } } // ── Accessing via ContentResolver (any app) ──────────────────── val cursor = contentResolver.query( Uri.parse("content://com.example.books/books"), arrayOf("_id", "title", "author"), null, null, "title ASC" ) cursor?.use { c -> while (c.moveToNext()) { val title = c.getString(c.getColumnIndexOrThrow("title")) } }
FileProvider — safe file sharing
The most common real-world use of ContentProvider is FileProvider — sharing files with other apps (camera, email, share sheet) without exposing your app's file system. FileProvider is a ContentProvider subclass provided by AndroidX that handles all the URI generation and permission granting automatically.
// AndroidManifest.xml // <provider android:name="androidx.core.content.FileProvider" // android:authorities="${applicationId}.fileprovider" // android:exported="false" android:grantUriPermissions="true"> // <meta-data android:name="android.support.FILE_PROVIDER_PATHS" // android:resource="@xml/file_paths"/> // </provider> // res/xml/file_paths.xml // <paths> <cache-path name="images" path="images/"/> </paths> // In code — share a file val file = File(cacheDir, "images/photo.jpg") val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file) val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "image/jpeg" putExtra(Intent.EXTRA_STREAM, uri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // grants read to receiver } startActivity(Intent.createChooser(shareIntent, "Share image"))
An Intent is a messaging object used to request an action from another component. It can carry data (action, data URI, type, extras, flags) and target a specific component (explicit) or let the system find the best match (implicit).
// ── Explicit Intent ──────────────────────────────────────────── val explicit = Intent(this, DetailActivity::class.java).apply { putExtra("product_id", "prod_42") // extras — arbitrary key-value data putExtra("show_reviews", true) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) // behavior flags } startActivity(explicit) // ── Implicit Intent — open URL ───────────────────────────────── val viewUrl = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com")) startActivity(viewUrl) // browser or WebView app handles this // ── Implicit Intent — share text ─────────────────────────────── val share = Intent(Intent.ACTION_SEND).apply { type = "text/plain" // MIME type narrows the match putExtra(Intent.EXTRA_TEXT, "Check this out!") } startActivity(Intent.createChooser(share, "Share via")) // ── Always check if an implicit intent can be handled ────────── if (viewUrl.resolveActivity(packageManager) != null) { startActivity(viewUrl) // safe — at least one app can handle it } else { showMessage("No app available to handle this action") } // ── Intent Filter (in manifest — receiving implicit intents) ─── // <activity android:name=".ShareReceiver"> // <intent-filter> // <action android:name="android.intent.action.SEND"/> // <category android:name="android.intent.category.DEFAULT"/> // <data android:mimeType="image/*"/> // </intent-filter> // </activity>
PendingIntent — deferred execution
A PendingIntent wraps an Intent and grants another app (typically the system) the right to fire it on your app's behalf, at a future time. Used for notifications, AlarmManager, and widget actions.
val intent = Intent(this, MainActivity::class.java).apply { putExtra("notification_id", notifId) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) } val pendingIntent = PendingIntent.getActivity( this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("New message") .setContentIntent(pendingIntent) // fires when notification is tapped .setAutoCancel(true) .build()
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <!-- ① Permissions — declared before use ─────────────────── --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- ② Hardware features ──────────────────────────────────── --> <uses-feature android:name="android.hardware.camera" android:required="false"/> <application android:name=".MyApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:theme="@style/Theme.App" android:allowBackup="true"> <!-- ③ Activity ───────────────────────────────────────── --> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <!-- Deep link --> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="https" android:host="myapp.com"/> </intent-filter> </activity> <!-- ④ Service ────────────────────────────────────────── --> <service android:name=".MusicPlayerService" android:exported="false" android:foregroundServiceType="mediaPlayback"/> <!-- ⑤ BroadcastReceiver ─────────────────────────────── --> <receiver android:name=".BootReceiver" android:exported="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> <!-- ⑥ ContentProvider ───────────────────────────────── --> <provider android:name=".BookProvider" android:authorities="${applicationId}.books" android:exported="false"/> </application> </manifest>
Key manifest attributes
| Attribute | Applies to | Meaning |
|---|---|---|
| android:exported | All components | Whether other apps can start this component. Must be explicitly declared on API 31+. Default false for Services and Receivers. Must be true for any component with an intent-filter that other apps use. |
| android:launchMode | Activity | standard, singleTop, singleTask, singleInstance. Controls back stack and instance creation behavior. |
| android:permission | All components | Callers must hold this permission to start/bind the component. Enforced by the OS — no code required in the component itself. |
| android:process | All components | Run this component in a separate process. Each process gets its own VM and memory space. Colons prefix = private; no colon = shared with other apps. |
| android:foregroundServiceType | Service | Required for foreground services on Android 10+. Values: camera, microphone, location, mediaPlayback, phoneCall, dataSync, etc. |
| android:authorities | ContentProvider | Unique identifier for the provider. Must be globally unique — use your package name as a prefix. Used in content:// URIs. |
| android:grantUriPermissions | ContentProvider | Allows the provider to grant temporary read/write URI permissions to other apps. Required for FileProvider. |
The four core components have not been replaced — they are still the only entry points the OS uses. But the implementation patterns inside each component have evolved dramatically. Fragments, ViewModel, Compose, WorkManager, and Jetpack Navigation all build on top of the component model rather than replacing it.
// Single Activity — container for all navigation destinations class MainActivity : AppCompatActivity() { private val binding by viewBinding(ActivityMainBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) // NavHostFragment manages all screen navigation // Each screen is a Fragment or Compose NavHost destination // Activity only handles: permissions, back press, window insets } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Deep link handling — NavController routes to the right screen findNavController(R.id.nav_host).handleDeepLink(intent) } } // activity_main.xml // <FragmentContainerView android:name="androidx.navigation.fragment.NavHostFragment" // app:navGraph="@navigation/nav_graph" // app:defaultNavHost="true"/> // ── With Compose — NavHost replaces FragmentContainerView ────── class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val navController = rememberNavController() AppNavHost(navController = navController) // all screens in Compose } } }
The component model survives all of this. Jetpack Compose doesn't replace Activities — it runs inside them. ViewModels don't replace Services — they run within the Activity lifecycle. Navigation Component doesn't replace the OS task back stack — it adds a fragment/compose layer on top. The four components remain the foundation everything else builds on.
| Property | Activity | Service | BroadcastReceiver | ContentProvider |
|---|---|---|---|---|
| Has UI? | Yes | No | No | No |
| Started by | Intent (explicit/implicit), launcher, notification | startService(), bindService(), system | sendBroadcast(), system events | ContentResolver queries |
| Runs on main thread? | Yes | Yes — use coroutines! | Yes — max 10 seconds | No — caller's thread |
| Time limit | None (user-driven) | None (foreground); background limited by OS | 10 seconds strictly | No strict limit but should be fast |
| Survives app background? | No (stopped, may be killed) | Yes (foreground); maybe (background) | Briefly (10s) | Yes (process independent) |
| Needs manifest declaration? | Yes | Yes | Yes (static) or No (dynamic) | Yes |
| Cross-app access? | Yes (exported=true) | Yes (exported=true) | Yes (implicit intents) | Yes (primary use case) |
| Process kill impact | Recreated from saved state bundle | Restarted per START_ policy | Lost if not yet started | Recreated when queried |
| Modern alternative | Still required (Compose inside it) | WorkManager (most cases) | WorkManager constraints | FileProvider / Room |
| Best used for | Every screen — the core UI unit | Music, nav, downloads, FG work | Boot, alarms, cross-app events | File sharing, system integration |